iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Software Development

GPU 效能優化實戰:30 天從 Kernel 到 Profiling (重賽版)系列 第 21

Day 21:先把 LCN 說清楚,再談 GPU }重賽版{

  • 分享至 

  • xImage
  •  

mur mur 我放在最下面 抱歉讓人見笑ㄌ 哈哈哈

這幾天換了一個完全新的題目水 (嘿嘿嘿
笑爛 但總之是個例子 也是我自己手刻(我跟AI一起 簡稱 我自己手刻 哈哈哈哈)
老黃曆了 先說個數學 在講個例子 然後提優化 最後會有一些我的小廢話
ok here we 夠~


OK 等一下 先不要go
我打完之後發現 狗e04長 但是 基本上不是太難的東西 大概吧?
總之 先不要被長度下到


Day 21:從 Graph、Node、Edge 開始,LCN 到底要解什麼?

前面幾天,我們一直在看 GEMM、FWT、Tensor Core,以及怎麼把一段計算塞進 GPU。

今天換一個看起來完全不像矩陣乘法的問題:我們要在平面上畫一張 graph,重新安排每個 node 的位置,讓某一條 edge 不要被其他 edges 穿過太多次。

如果你沒有接觸過圖論,這句話裡可能每個名詞都需要先解釋。所以今天先不急著碰 CUDA,我們從「什麼是 graph」開始。


Graph 不是統計圖表,而是一組物件與關係

圖論裡的 graph,不是折線圖、長條圖或圓餅圖。

它是一種描述「哪些物件彼此有關係」的資料結構:

Graph = Nodes + Edges

node 是我們關心的物件,也常被叫做 vertex。

例如:

社群網路
    node = 使用者

交通網路
    node = 車站

電路圖
    node = 元件或接點

這個 LCN 題目
    node = 要放在畫布上的一個點

edge 則表示兩個 nodes 之間存在連接關係:

社群網路
    edge = 兩個人互相認識

交通網路
    edge = 兩個車站之間有路線

電路圖
    edge = 兩個接點之間有導線

這個 LCN 題目
    edge = 兩個 nodes 之間必須畫一條直線

假設有四個 nodes:

V = {0, 1, 2, 3}

並且有四條 edges:

E = {
    (0, 1),
    (0, 2),
    (1, 3),
    (2, 3)
}

我們可以先把連接關係畫成:

node 0 ●────────● node 1
       |        |
       |        |
node 2 ●────────● node 3

這張小圖告訴我們:

node 0 連到 node 1、node 2
node 1 連到 node 0、node 3
node 2 連到 node 0、node 3
node 3 連到 node 1、node 2

Graph 通常寫成:

G = (V, E)

V = 所有 nodes 的集合
E = 所有 edges 的集合

到這裡,我們只有定義「誰和誰相連」。那四個 nodes 一定要排成正方形嗎?不一定。


Graph 固定,但 Layout 可以一直改

同一張 graph 可以有很多種畫法。

我們可以把 node 0 放左上、node 1 放右下;也可以把 node 0 移到中央。只要 edges 仍然連接原本指定的端點,graph topology 就沒有改變。

不能修改的東西
    哪些 nodes 存在
    哪些 node pairs 之間有 edge

可以修改的東西
    每個 node 的 x 座標
    每個 node 的 y 座標

每個 node 的位置可以寫成:

pos[v] = (x[v], y[v])

所有 nodes 的位置合在一起,就是一份 layout:

layout = {
    pos[0],
    pos[1],
    pos[2],
    ...
}

一條 edge e = (u, v) 則會被畫成 pos[u]pos[v] 之間的直線線段:

node u ●────────────────● node v
       <---- edge e ---->

這個差別非常重要:

graph
    決定誰和誰相連

layout
    決定這些 nodes 被放在哪裡

drawing
    依照 layout,把每條 edge 畫成直線

Graph 沒有改,layout 一改,edges 在畫布上的走向就會跟著改。有些 layouts 很乾淨,有些 layouts 會讓大量 edges 互相穿越。

LCN 要搜尋的正是 layout。


所以這個題目到底要我們做什麼?

輸入會給我們:

一張固定的 graph G = (V, E)
一個 width * height 的整數畫布

我們要替每個 node 選擇一組整數座標:

node v -> pos[v] = (x[v], y[v])

再把每一條 edge 畫成直線:

edge (u, v) -> segment(pos[u], pos[v])

最後要同時做到兩件事:

第一件事
    layout 必須合法
    nodes 不能重疊、不能跑出畫布
    node 不能落在別人的 edge 上
    edges 不能疊成同一段線

第二件事
    讓 crossing 最嚴重的那一條 edge
    被穿過的次數越少越好

可以先把完整題目壓成一行:

在所有合法的直線 graph layouts 中,
找到「單一 edge 最大 crossing 數」最小的 layout。

這句話就是後面所有 Crossing、Layout 與 SA 實作共同要解的問題。

現在我們可以來看一個有點奇怪的例子。

假設兩個 layouts 都有 4 個 crossing pairs:

Layout A 的 per-edge crossing counts

[4, 1, 1, 1, 1]

總 crossing pairs = (4 + 1 + 1 + 1 + 1) / 2
                   = 4

另一張圖則是:

Layout B 的 per-edge crossing counts

[2, 2, 2, 2]

總 crossing pairs = (2 + 2 + 2 + 2) / 2
                   = 4

兩張圖的 crossings 總數完全一樣。

如果我們只做一般的 crossing minimization,它們可能會被判成平手。但 LCN 會選 Layout B,因為 Layout A 有一條 edge 被穿過 4 次,Layout B 最嚴重的 edge 只被穿過 2 次。

Layout A -> K = 4
Layout B -> K = 2

這就是 LCN 最核心的想法:

不要只問整張圖總共有多少 crossings。

要問的是:
哪一條 edge 最慘?它被穿過了幾次?

Day 21 先把這個問題定義完整。今天不急著寫 CUDA kernel,也不急著談 simulated annealing。因為如果連「什麼答案比較好」都沒有固定下來,後面做的每一個 GPU 優化都可能只是在加速另一個問題。


LCN 的 L,指的是 Local

對一個已經畫好的 layout,我們先數每一條 edge 被多少其他 edges 穿過:

c[e] = edge e 的 crossing count

再找出其中最大的值:

K(layout) = max(c[e])

這個 K 是這份 layout 的 local crossing number。它觀察的是單一 edge 承受的最壞情況,所以叫做 Local Crossing Number。

真正的圖論問題,還要在所有合法 layouts 裡面找最小值:

LCN(G) = min(
    K(layout)
    for every legal layout of graph G
)

所以這裡其實有兩層問題:

評分問題
    給定一份 layout,算出它的 K

搜尋問題
    在大量合法 layouts 裡,找到 K 最小的那一份

前者需要 Crossing evaluator;後者需要 Layout 與 SA。這也會成為後面幾篇文章的三條主線。


實際輸入:Solver 讀到的是 graph 資料

輸入資料大致包含:

width
height
nodes
edges

一份縮小過的 JSON 可以長這樣:

{
  "width": 10,
  "height": 10,
  "nodes": [
    {"id": 0, "x": 1, "y": 1},
    {"id": 1, "x": 9, "y": 9},
    {"id": 2, "x": 1, "y": 9},
    {"id": 3, "x": 9, "y": 1}
  ],
  "edges": [
    {"id": 0, "source": 0, "target": 1},
    {"id": 1, "source": 2, "target": 3}
  ]
}

理論中的 VEpos[v],到了程式裡就是 nodesedgesxy。後面的 CPU 與 GPU 實作,最後都會把這些欄位整理成連續 arrays 來計算。


兩條線交在一起,就一定算 crossing 嗎?

先把剛才的四個 nodes 畫出來:

node 2 ●       ● node 1
         \     /
          \   /
           \ /
           / \
          /   \
         /     \
node 0 ●       ● node 3

存在的 edges 是:

edge 0 = (0, 1)
edge 1 = (2, 3)

兩條線段在內部穿過彼此,因此這裡有一個 crossing pair:

c[0] = 1
c[1] = 1
K    = 1

注意:一個 crossing 會同時記在兩條 edges 上。

一個 crossing pair
    -> edge 0 的 count 加 1
    -> edge 1 的 count 加 1

所以:

sum(c[e]) = 2 * crossing_pair_count

這也是之後檢查程式是否算錯時,很好用的一個 invariant。

但是「兩條線碰在一起」有很多不同情況。只有其中一種會成為合法 layout 裡的 crossing。


情況一:穿過線段內部,算 crossing

這是最標準的 crossing:

 A ●       ● C
     \     /
      \   /
       \ /
       / \
      /   \
     /     \
 D ●       ● B

兩條 edges 不共用 node,交點也不在任何端點上。

edge AB 與 edge CD
    -> proper interior crossing
    -> crossing count 加 1

情況二:兩條 edges 共用 node,不算 crossing

      B ●
       /
      /
 A ●---------● C

edges ABAC 都從 node A 出發。它們在 A 接起來,是 graph topology 本來就要求的連接,不是兩條 edges 互相穿越。

edge AB 與 edge AC
    -> shared endpoint
    -> 不算 crossing

如果忘記先排除共用端點,高 degree node 周圍會被灌進大量假的 crossings。


情況三:node 落在別人的 edge 上,這不是好成績

A ●---------● C ---------● B

假設 edge 是 AB,而 node C 並不是它的端點。C 剛好落在線段 AB 的內部。

這不能被當成「沒有 proper crossing,所以是 0」。它是一份不合法的 layout:

node C lies on edge AB
    -> V3 violation
    -> reject layout

情況四:兩條 edges 疊在一起,也不是 0 crossing

A ●--------------● B
        C ●--------------● D

如果兩條共線線段共享一段正長度區間,它們不是在一個點穿越,而是重疊。

edge AB overlaps edge CD
    -> V4 violation
    -> reject layout

這裡有一個很重要的區別:

crossing predicate 回傳 false

不一定代表 layout 合法。它也可能代表這根本不是 crossing evaluator 應該接受的幾何情況。


先檢查 V1~V4,再計算 K

這個專案把必要的幾何合法性分成四類:

V1:node 必須嚴格位於畫布內

    1 <= x <= width  - 1
    1 <= y <= height - 1

V2:不同 nodes 不能使用同一組座標

    pos[u] != pos[v] for u != v

V3:node 不能落在非 incident edge 的內部

V4:兩條不同 edges 不能重疊一段正長度區間

validator 另外會檢查 directed edge 是否向上:

V5:y[target] > y[source]

在目前專案的 admission contract 裡,V1~V4 是 error;V5 被記成 warning,不會讓 official_cpu_k(...) 直接拒絕 layout。這個差別要說清楚,否則很容易把「幾何合法」與「符合 upward drawing 偏好」混在一起。

完整的評分順序是:

candidate layout
        |
        v
檢查 V1~V4
        |
        +---- 有 error ----> reject
        |
        v
計算每條 edge 的 crossing count
        |
        v
K = max(c[e])

這也解釋了一個最簡單的作弊方法為什麼行不通:把所有 nodes 放在同一點,crossing evaluator 也許看不到 proper crossing,但 V1~V4 validator 會先把它擋掉。

crossing count = 0

和:

這是一份合法、而且 K = 0 的 layout

是兩件不同的事。


程式怎麼判斷 proper crossing?

對兩條 edges:

edge e = (a, b)
edge f = (c, d)

我們先排除共用端點:

if a == c or a == d or b == c or b == d:
    not a crossing

接著要知道 C、D 分別落在直線 AB 的哪一側。這可以用二維 orientation:

orient(A, B, C) =
    (B.x - A.x) * (C.y - A.y)
  - (B.y - A.y) * (C.x - A.x)

結果的正負號代表 C 位於 AB 的哪一側:

orient(A, B, C) > 0 -> 一側
orient(A, B, C) < 0 -> 另一側
orient(A, B, C) = 0 -> 三點共線

兩條線段在內部互相穿越,需要同時滿足:

C 與 D 位於 AB 的不同側

而且

A 與 B 位於 CD 的不同側

把它寫成接近程式碼的形式:

opposite(x, y) =
    (x < 0 and y > 0)
    or
    (x > 0 and y < 0)

proper_crossing(e, f) =
    opposite(
        orient(a, b, c),
        orient(a, b, d)
    )
    and
    opposite(
        orient(c, d, a),
        orient(c, d, b)
    )

只比較正負號,不需要計算 slope,也沒有垂直線除以零的問題。

至於共線、重疊、node-on-edge 等退化情況,則交給前面的 geometry validator 判斷。這樣 crossing evaluator 與合法性檢查各自有清楚的責任。


LCN 最小化的是 K,solver 為什麼還保存四個數字?

LCN 的主要目標非常明確:

minimize K

但是搜尋時,很多 layouts 會有相同的 K。假設目前已經找到 K = 3,下一個 candidate 也是 K = 3,我們仍然需要知道它是不是往比較好的方向移動。

因此 solver 保存:

K   = 最大的 per-edge crossing count
n_K = 有多少條 edge 的 crossing count 等於 K
Phi = 所有 per-edge crossing count 的平方和
C   = crossing pairs 總數

計算方式是:

K = max(c[e])

n_K = count(c[e] == K)

Phi = sum(c[e] * c[e])

C = sum(c[e]) / 2

K = 0 時,專案把 n_K 也記成 0,避免把「所有零 crossing edges 的數量」當成仍需改善的 bottleneck 數量。

再按照下面的順序比較:

objective = (K, n_K, Phi, C)

數字越小越好
由左到右比較
前一欄相同,才比較下一欄

例如:

Layout X counts = [3, 3, 0]

K   = 3
n_K = 2
Phi = 18
C   = 3

另一份 layout:

Layout Y counts = [3, 1, 1, 1]

K   = 3
n_K = 1
Phi = 12
C   = 3

兩者的 K 和總 crossing pairs 都相同,但 Layout Y 只有一條 edge 卡在最壞值,所以 solver 會先選 Y。

這些 tie-breakers 是搜尋器用來辨認方向的工程目標;LCN 的主成績仍然是最前面的 K。不能為了讓 C 下降,接受一份 K 更差的 best solution。


把完整問題寫成一段程式規格

到這裡,我們可以把 Day 21 的數學問題濃縮成:

given:
    graph G = (V, E)
    integer canvas width, height

choose:
    integer position pos[v] = (x[v], y[v])
    for every node v

subject to:
    V1: every node is strictly inside the canvas
    V2: no two nodes share a position
    V3: no node lies inside another edge
    V4: no two distinct edges overlap on a segment

compute:
    c[e] = number of proper crossings on edge e
    K    = max(c[e])

minimize:
    K

solver tie-break:
    (K, n_K, Phi, C)

整個求解流程則是:

flowchart TD
    A[讀入固定的 Graph] --> B[產生一份 node layout]
    B --> C{V1 到 V4 合法嗎}
    C -->|否| D[拒絕或修復]
    C -->|是| E[計算所有 edge crossings]
    E --> F[建立每條 edge 的 c e]
    F --> G[得到 K、n K、Phi、C]
    G --> H{比目前的解好嗎}
    H -->|是| I[保存 best layout]
    H -->|否| J[由 SA 決定接受或拒絕]
    I --> K[提出下一個 layout]
    J --> K
    K --> C

後面要加速的,就是這張圖中的三大區域:

Crossing
    快速算出 c[e]、K 與 tie-break objective

Layout
    產生比較有希望、而且容易修成合法的初始位置

Simulated Annealing
    在龐大的 layout 空間裡持續提出與篩選 moves

它們需要的 GPU 策略完全不同。Crossing 是大量 edge-pair predicates;Layout 同時包含規則的 all-pairs forces 與不規則的 adjacency;SA 則有狀態,而且下一步依賴這一步接受了什麼。

所以這個案例好玩的地方,是我們不能只把三段 for-loop 改成 CUDA,就宣布 GPU 加速完成。


下一篇,從最笨但最可信的 Crossing 開始

今天我們只回答了一件事:LCN 到底要找什麼答案。

合法 layout
    -> 數出每條 edge 的 crossing count
    -> 找到最壞的 edge
    -> 最小化 K

Day 22 會開始寫程式:先用 CPU 雙迴圈檢查所有 edge pairs,再把同一個工作直接搬到 GPU。

那一版會做很多重複工作,甚至故意把同一對 edges 算兩次。但它會提供一條非常重要的 correctness baseline,也會帶出第一個反直覺的 GPU 問題:

少算一次 crossing predicate

和

避免很多 threads 競爭同一個 counter

哪一個比較重要?

有了今天固定下來的數學與合法性 contract,我們下一篇才有資格談「怎樣算得更快」。

程式對照

mur mur time-----
下面是我發稿當天 沒帶電腦 當場跟活動的兄弟 借了一台電腦
哈哈哈 超感謝
標題甚至是亂打
內容純是AI逐字稿
總之 我覺得很荒謬
哈哈哈哈
:

qwertyuio

所以就是,呃,不好意思,我今天在外面,这边在外面,甚至是用语音录的稿。我很感谢那个,叫什么名字来着,叫什么名字来着,哈比,金豪,哈比。谢谢金豪,哈比,借我他的笔电,帮我这个ID帮我贴文。我的二十一天是他维持的,对啊,OK。干,我今天還开会,靠北。

先預告一下,我等下會再更新。我今天講的題目是那個叫做LCN least crossing number。那圖它去解決一個叫做 LCN 問題。LCN 問題是在講的是 least crossing number。你可以想是一個圖,一個圖本身它有很多的線跟 edge 連線在一起。這個 edge 連線在一起,本身它每個 edge 會有重疊。那 edge 的重疊當中,edge 跟 edge 之間你會要找某個 edge,它的節點是最……它跨過的線是最多的。那我們要讓這個跨過最多的線本身它最少化,要怎麼去做。這幾天,六天、九天、八天,隨便,大概會講這樣子的一個問題,然後用 GPU 加速去怎麼解決,然後我們會嘗試去從它的那個交換,到它的那個算法,到 SA,到怎麼去 parallelize SA 這件事情,大概會有這樣子的過程去講這樣的 GPU 加速。對。那我現在還在外面,再次謝謝金航,借我他的筆電。


上一篇
Day 20:再做一次比較激進的版本]重賽版[
下一篇
Day 22:先把 Crossing 算對,再談怎麼少算 ))重賽版((
系列文
GPU 效能優化實戰:30 天從 Kernel 到 Profiling (重賽版)26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言